Passed
Branch wavefile-reader (a1c410)
by Rafael S.
03:00
created

WaveFile.setCuePoint   B

Complexity

Conditions 5

Size

Total Lines 35
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 27
dl 0
loc 35
rs 8.7653
c 0
b 0
f 0
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import {encode, decode} from 'base64-arraybuffer-es6';
33
import WaveFileConverter from './lib/wavefile-converter';
34
import fixRIFFTag from './lib/fix-riff-tag';
35
36
/**
37
 * A class to manipulate wav files.
38
 * @extends WaveFileConverter
39
 */
40
export default class WaveFile extends WaveFileConverter {
41
42
  /**
43
   * @param {?Uint8Array=} wavBuffer A wave file buffer.
44
   * @throws {Error} If container is not RIFF, RIFX or RF64.
45
   * @throws {Error} If format is not WAVE.
46
   * @throws {Error} If no 'fmt ' chunk is found.
47
   * @throws {Error} If no 'data' chunk is found.
48
   */
49
  constructor(wavBuffer=null) {
50
    super();
51
    if (wavBuffer) {
52
      this.fromBuffer(wavBuffer);
53
    }
54
  }
55
56
  /**
57
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
58
   * @param {string} base64String A .wav file as a base64 string.
59
   * @throws {Error} If any property of the object appears invalid.
60
   */
61
  fromBase64(base64String) {
62
    this.fromBuffer(new Uint8Array(decode(base64String)));
63
  }
64
65
  /**
66
   * Return a base64 string representig the WaveFile object as a .wav file.
67
   * @return {string} A .wav file as a base64 string.
68
   * @throws {Error} If any property of the object appears invalid.
69
   */
70
  toBase64() {
71
    /** @type {!Uint8Array} */
72
    let buffer = this.toBuffer();
73
    return encode(buffer, 0, buffer.length);
74
  }
75
76
  /**
77
   * Return a DataURI string representig the WaveFile object as a .wav file.
78
   * The return of this method can be used to load the audio in browsers.
79
   * @return {string} A .wav file as a DataURI.
80
   * @throws {Error} If any property of the object appears invalid.
81
   */
82
  toDataURI() {
83
    return 'data:audio/wav;base64,' + this.toBase64();
84
  }
85
86
  /**
87
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
88
   * @param {string} dataURI A .wav file as DataURI.
89
   * @throws {Error} If any property of the object appears invalid.
90
   */
91
  fromDataURI(dataURI) {
92
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
93
  }
94
95
  /**
96
   * Return the value of a RIFF tag in the INFO chunk.
97
   * @param {string} tag The tag name.
98
   * @return {?string} The value if the tag is found, null otherwise.
99
   */
100
  getTag(tag) {
101
    /** @type {!Object} */
102
    let index = this.getTagIndex_(tag);
103
    if (index.TAG !== null) {
104
      return this.LIST[index.LIST].subChunks[index.TAG].value;
105
    }
106
    return null;
107
  }
108
109
  /**
110
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
111
   * then it is created. It if exists, it is overwritten.
112
   * @param {string} tag The tag name.
113
   * @param {string} value The tag value.
114
   * @throws {Error} If the tag name is not valid.
115
   */
116
  setTag(tag, value) {
117
    tag = fixRIFFTag(tag);
118
    /** @type {!Object} */
119
    let index = this.getTagIndex_(tag);
120
    if (index.TAG !== null) {
121
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
122
        value.length + 1;
123
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
124
    } else if (index.LIST !== null) {
125
      this.LIST[index.LIST].subChunks.push({
126
        chunkId: tag,
127
        chunkSize: value.length + 1,
128
        value: value});
129
    } else {
130
      this.LIST.push({
131
        chunkId: 'LIST',
132
        chunkSize: 8 + value.length + 1,
133
        format: 'INFO',
134
        subChunks: []});
135
      this.LIST[this.LIST.length - 1].subChunks.push({
136
        chunkId: tag,
137
        chunkSize: value.length + 1,
138
        value: value});
139
    }
140
  }
141
142
  /**
143
   * Remove a RIFF tag from the INFO chunk.
144
   * @param {string} tag The tag name.
145
   * @return {boolean} True if a tag was deleted.
146
   */
147
  deleteTag(tag) {
148
    /** @type {!Object} */
149
    let index = this.getTagIndex_(tag);
150
    if (index.TAG !== null) {
151
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
152
      return true;
153
    }
154
    return false;
155
  }
156
157
  /**
158
   * Return a Object<tag, value> with the RIFF tags in the file.
159
   * @return {!Object<string, string>} The file tags.
160
   */
161
  listTags() {
162
    /** @type {?number} */
163
    let index = this.getLISTINFOIndex_();
164
    /** @type {!Object} */
165
    let tags = {};
166
    if (index !== null) {
167
      for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
168
        tags[this.LIST[index].subChunks[i].chunkId] =
169
          this.LIST[index].subChunks[i].value;
170
      }
171
    }
172
    return tags;
173
  }
174
175
  /**
176
   * Return an array with all cue points in the file, in the order they appear
177
   * in the file.
178
   * The difference between this method and using the list in WaveFile.cue
179
   * is that the return value of this method includes the position in
180
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
181
   * @return {!Array<!Object>}
182
   */
183
  listCuePoints() {
184
    /** @type {!Array<!Object>} */
185
    let points = this.getCuePoints_();
186
    for (let i = 0, len = points.length; i < len; i++) {
187
      points[i].milliseconds =
188
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
189
    }
190
    return points;
191
  }
192
193
  /**
194
   * Create a cue point in the wave file.
195
   * @param {number} position The cue point position in milliseconds.
196
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
197
   */
198
  setCuePoint(position, labl='') {
199
    this.cue.chunkId = 'cue ';
200
    position = (position * this.fmt.sampleRate) / 1000;
201
    /** @type {!Array<!Object>} */
202
    let existingPoints = this.getCuePoints_();
203
    this.clearLISTadtl_();
204
    /** @type {number} */
205
    let len = this.cue.points.length;
206
    this.cue.points = [];
207
    /** @type {boolean} */
208
    let hasSet = false;
209
    if (len === 0) {
210
      this.setCuePoint_(position, 1, labl);
211
    } else {
212
      for (let i = 0; i < len; i++) {
213
        if (existingPoints[i].dwPosition > position && !hasSet) {
214
          this.setCuePoint_(position, i + 1, labl);
215
          this.setCuePoint_(
216
            existingPoints[i].dwPosition,
217
            i + 2,
218
            existingPoints[i].label);
219
          hasSet = true;
220
        } else {
221
          this.setCuePoint_(
222
            existingPoints[i].dwPosition,
223
            i + 1,
224
            existingPoints[i].label);
225
        }
226
      }
227
      if (!hasSet) {
228
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
229
      }
230
    }
231
    this.cue.dwCuePoints = this.cue.points.length;
232
  }
233
234
  /**
235
   * Remove a cue point from a wave file.
236
   * @param {number} index the index of the point. First is 1,
237
   *    second is 2, and so on.
238
   */
239
  deleteCuePoint(index) {
240
    this.cue.chunkId = 'cue ';
241
    /** @type {!Array<!Object>} */
242
    let existingPoints = this.getCuePoints_();
243
    this.clearLISTadtl_();
244
    /** @type {number} */
245
    let len = this.cue.points.length;
246
    this.cue.points = [];
247
    for (let i = 0; i < len; i++) {
248
      if (i + 1 !== index) {
249
        this.setCuePoint_(
250
          existingPoints[i].dwPosition,
251
          i + 1,
252
          existingPoints[i].label);
253
      }
254
    }
255
    this.cue.dwCuePoints = this.cue.points.length;
256
    if (this.cue.dwCuePoints) {
257
      this.cue.chunkId = 'cue ';
258
    } else {
259
      this.cue.chunkId = '';
260
      this.clearLISTadtl_();
261
    }
262
  }
263
264
  /**
265
   * Update the label of a cue point.
266
   * @param {number} pointIndex The ID of the cue point.
267
   * @param {string} label The new text for the label.
268
   */
269
  updateLabel(pointIndex, label) {
270
    /** @type {?number} */
271
    let cIndex = this.getAdtlChunk_();
272
    if (cIndex !== null) {
273
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
274
        if (this.LIST[cIndex].subChunks[i].dwName ==
275
            pointIndex) {
276
          this.LIST[cIndex].subChunks[i].value = label;
277
        }
278
      }
279
    }
280
  }
281
282
  /**
283
   * Return an array with all cue points in the file, in the order they appear
284
   * in the file.
285
   * @return {!Array<!Object>}
286
   * @private
287
   */
288
  getCuePoints_() {
289
    /** @type {!Array<!Object>} */
290
    let points = [];
291
    for (let i = 0, len = this.cue.points.length; i < len; i++) {
292
      points.push({
293
        dwPosition: this.cue.points[i].dwPosition,
294
        label: this.getLabelForCuePoint_(
295
          this.cue.points[i].dwName)});
296
    }
297
    return points;
298
  }
299
300
  /**
301
   * Return the label of a cue point.
302
   * @param {number} pointDwName The ID of the cue point.
303
   * @return {string}
304
   * @private
305
   */
306
  getLabelForCuePoint_(pointDwName) {
307
    /** @type {?number} */
308
    let cIndex = this.getAdtlChunk_();
309
    if (cIndex !== null) {
310
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
311
        if (this.LIST[cIndex].subChunks[i].dwName ==
312
            pointDwName) {
313
          return this.LIST[cIndex].subChunks[i].value;
314
        }
315
      }
316
    }
317
    return '';
318
  }
319
320
  /**
321
   * Return the index of the INFO chunk in the LIST chunk.
322
   * @return {?number} the index of the INFO chunk.
323
   * @private
324
   */
325
  getLISTINFOIndex_() {
326
    /** @type {?number} */
327
    let index = null;
328
    for (let i = 0, len = this.LIST.length; i < len; i++) {
329
      if (this.LIST[i].format === 'INFO') {
330
        index = i;
331
        break;
332
      }
333
    }
334
    return index;
335
  }
336
337
  /**
338
   * Return the index of the 'adtl' LIST in this.LIST.
339
   * @return {?number}
340
   * @private
341
   */
342
  getAdtlChunk_() {
343
    for (let i = 0, len = this.LIST.length; i < len; i++) {
344
      if (this.LIST[i].format == 'adtl') {
345
        return i;
346
      }
347
    }
348
    return null;
349
  }
350
351
  /**
352
   * Return the index of a tag in a FILE chunk.
353
   * @param {string} tag The tag name.
354
   * @return {!Object<string, ?number>}
355
   *    Object.LIST is the INFO index in LIST
356
   *    Object.TAG is the tag index in the INFO
357
   * @private
358
   */
359
  getTagIndex_(tag) {
360
    /** @type {!Object<string, ?number>} */
361
    let index = {LIST: null, TAG: null};
362
    for (let i = 0, len = this.LIST.length; i < len; i++) {
363
      if (this.LIST[i].format == 'INFO') {
364
        index.LIST = i;
365
        for (let j=0, subLen = this.LIST[i].subChunks.length; j < subLen; j++) {
366
          if (this.LIST[i].subChunks[j].chunkId == tag) {
367
            index.TAG = j;
368
            break;
369
          }
370
        }
371
        break;
372
      }
373
    }
374
    return index;
375
  }
376
377
  /**
378
   * Push a new cue point in this.cue.points.
379
   * @param {number} position The position in milliseconds.
380
   * @param {number} dwName the dwName of the cue point
381
   * @private
382
   */
383
  setCuePoint_(position, dwName, label) {
384
    this.cue.points.push({
385
      dwName: dwName,
386
      dwPosition: position,
387
      fccChunk: 'data',
388
      dwChunkStart: 0,
389
      dwBlockStart: 0,
390
      dwSampleOffset: position,
391
    });
392
    this.setLabl_(dwName, label);
393
  }
394
395
  /**
396
   * Clear any LIST chunk labeled as 'adtl'.
397
   * @private
398
   */
399
  clearLISTadtl_() {
400
    for (let i = 0, len = this.LIST.length; i < len; i++) {
401
      if (this.LIST[i].format == 'adtl') {
402
        this.LIST.splice(i);
403
      }
404
    }
405
  }
406
407
  /**
408
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
409
   * @param {number} dwName The ID of the cue point.
410
   * @param {string} label The label for the cue point.
411
   * @private
412
   */
413
  setLabl_(dwName, label) {
414
    /** @type {?number} */
415
    let adtlIndex = this.getAdtlChunk_();
416
    if (adtlIndex === null) {
417
      this.LIST.push({
418
        chunkId: 'LIST',
419
        chunkSize: 4,
420
        format: 'adtl',
421
        subChunks: []});
422
      adtlIndex = this.LIST.length - 1;
423
    }
424
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
425
  }
426
427
  /**
428
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
429
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
430
   * @param {number} dwName The ID of the cue point.
431
   * @param {string} label The label for the cue point.
432
   * @private
433
   */
434
  setLabelText_(adtlIndex, dwName, label) {
435
    this.LIST[adtlIndex].subChunks.push({
436
      chunkId: 'labl',
437
      chunkSize: label.length,
438
      dwName: dwName,
439
      value: label
440
    });
441
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
442
  }
443
}
444